package com.kdgc.energy.dmm.update;

import com.kdgc.energy.dmm.Environment;
import com.kdgc.energy.dmm.condition.Condition;
import com.kdgc.energy.dmm.lambda.SerializedFunction;
import com.kdgc.energy.dmm.table.Table;
import com.kdgc.energy.dmm.time.TimeStatistical;
import org.apache.metamodel.UpdateCallback;
import org.apache.metamodel.UpdateSummary;
import org.apache.metamodel.data.Style;
import org.apache.metamodel.jdbc.JdbcDataContext;
import org.apache.metamodel.query.FilterItem;
import org.apache.metamodel.query.OperatorType;
import org.apache.metamodel.query.SelectItem;
import org.apache.metamodel.schema.Column;
import org.apache.metamodel.schema.MutableColumn;
import org.apache.metamodel.update.RowUpdationBuilder;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;

/**
 * @author xu.wenchang
 * @version 1.0 2022/04/18
 */
public class Update {
    private Class<?> clazz;
    private org.apache.metamodel.update.Update delegate;
    private TimeStatistical timeStatistical;
    
    public Update(Class<?> clazz) {
        this.clazz = clazz;
        this.delegate = new org.apache.metamodel.update.Update(Environment.get(clazz)
                                                                          .getTable());
        this.timeStatistical = new TimeStatistical();
        this.timeStatistical.phaseStart("build");
    }
    
    public <T, R> Update set(SerializedFunction<T, R> sf, Object value) {
        this.delegate.value(Environment.get(sf), value);
        return this;
    }
    
    public Update set(Object obj) {
        Table table = Environment.get(this.clazz);
        Field pkField = table.getPkField();
        
        if (null == pkField) {
            throw new UnsupportedOperationException(this.clazz.getName() + "未定义@Id注解，不能进行此操作");
        }
        
        if (this.clazz.isAssignableFrom(obj.getClass())) {
            Field[] fields = obj.getClass()
                                .getDeclaredFields();
            for (Field f : fields) {
                if (Modifier.isStatic(f.getModifiers())) {
                    continue;
                }
                
                f.setAccessible(true);
                
                try {
                    if (f.getName()
                         .equals(pkField.getName())) {
                        this.delegate.where(new FilterItem(new SelectItem(table.getColumnByField(f.getName())), OperatorType.EQUALS_TO, f.get(obj)));
                    } else {
                        this.delegate.value(table.getColumnByField(f.getName()), f.get(obj));
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
        
        if (obj instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) obj;
            
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                String key = entry.getKey()
                                  .toString();
                
                if (key.equals(pkField.getName())) {
                    this.delegate.where(new FilterItem(new SelectItem(table.getColumnByField(key)), OperatorType.EQUALS_TO, entry.getValue()));
                } else {
                    MutableColumn col = table.getColumnByField(key);
                    
                    if (null != col) {
                        this.delegate.value(col, entry.getValue());
                    }
                }
            }
        }
        
        return this;
    }
    
    public Update where(Condition condition) {
        this.delegate.where(condition.getDelegate());
        return this;
    }
    
    public int execute() {
        JdbcDataContext dataContext = new JdbcDataContext(Environment.getDataSource());
        UpdateSummary summary = dataContext.executeUpdate(updateCallback -> {
            Update.this.timeStatistical.phaseEnd("build");
            Update.this.timeStatistical.phaseStart("execute");
            
            try {
                String sql = Update.this.toSQL(updateCallback);
                Update.this.timeStatistical.setName(sql);
                
                if (Environment.isShowSQL()) {
                    Environment.getWriter()
                               .write(sql);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            
            Update.this.delegate.run(updateCallback);
            Update.this.timeStatistical.phaseEnd("execute");
        });
        
        this.timeStatistical.end();
        
        if (Environment.isStatExecTime()) {
            Environment.getWriter()
                       .write(this.timeStatistical.statisticalInfo());
        }
        
        return summary.getUpdatedRows()
                      .orElse(0);
    }
    
    @SuppressWarnings("unchecked")
    private String toSQL(UpdateCallback callback) throws Exception {
        Field whereItemField = org.apache.metamodel.update.Update.class.getDeclaredField("_whereItems");
        whereItemField.setAccessible(true);
        List<FilterItem> whereItems = (List<FilterItem>) whereItemField.get(this.delegate);
        RowUpdationBuilder updateBuilder = callback.update(this.delegate.getTable())
                                                   .where(whereItems);
        
        Class<?> superClass = org.apache.metamodel.update.Update.class.getSuperclass();
        Field columnsField = superClass.getDeclaredField("_columns");
        columnsField.setAccessible(true);
        Column[] columns = (Column[]) columnsField.get(this.delegate);
        
        Field valueField = superClass.getDeclaredField("_values");
        valueField.setAccessible(true);
        Object[] values = (Object[]) valueField.get(this.delegate);
        
        Field stylesField = superClass.getDeclaredField("_styles");
        stylesField.setAccessible(true);
        Style[] styles = (Style[]) stylesField.get(this.delegate);
        
        Field explicitNullsField = superClass.getDeclaredField("_explicitNulls");
        explicitNullsField.setAccessible(true);
        boolean[] explicitNulls = (boolean[]) explicitNullsField.get(this.delegate);
        
        for (int i = 0; i < columns.length; ++i) {
            Object value = values[i];
            Column column = columns[i];
            Style style = styles[i];
            if (value == null) {
                if (explicitNulls[i]) {
                    updateBuilder = updateBuilder.value(column, value, style);
                }
            } else {
                updateBuilder = updateBuilder.value(column, value, style);
            }
        }
        
        return updateBuilder.toSql();
    }
}
